// Copyright (c) 2011 The Kiwii Authors. All rights reserved.
// Use of this source code is governed by the New BSD License that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.

#ifndef KIWI_EVENTLOOP_INCLUDED
#define KIWI_EVENTLOOP_INCLUDED

#include "net/Event.hxx"
#include <sys/time.h>
#include <memory>
#include <vector>
#include <queue>
#include <boost/assert.hpp>

namespace kiwii {
  namespace net {

    template <typename PollScheme> //poll scheme like epoll on Linux, kqueue on *BSD, etc.
    class EventLoop : public EventLoopBase {
    public:
      EventLoop();
      virtual ~EventLoop() {}

      // noncopyable
      EventLoop(EventLoop const& rhs) = delete;
      EventLoop& operator=(EventLoop const& rhs) = delete;

      //If the fd has been registered, this call will modify the associated handler.
      //Note: if the handler is 0, the fd will still remain registered,
      //      and the handler will be called even if it has no effect
      //      when the associated event is delivered.
      //      Use delIOEvent() to unregister a fd.
      void setIOEvent(int fd, unsigned int events, IOEventHandler const& handler);

      //Delete the events associated with the fd.
      void delIOEvent(int fd, unsigned int events);

      //Add time event handler which will be triggered in milliseconds.
      //Return the identifier of this time event, which can be used to delete the time event.
      int setTimeEvent(long long milliseconds, TimeEventHandler const& handler) {
	TimeEvent te(milliseconds, handler);
	timeEvents_.push(te);
	return te.id_;
      }

      //enter event loop
      void rock();
      
      //stop event loop
      void stop() { keepLoop_ = false; }

    protected:
      PollScheme poll_;
      bool keepLoop_;
      std::vector<IOEvent> ioEvents_; // use fd as its index
      std::priority_queue<TimeEvent> timeEvents_;
    };

    template <typename P>
    EventLoop<P>::EventLoop() : keepLoop_(false) {
      ioEvents_.reserve(1024);
    }

    template <typename P>
    void EventLoop<P>::setIOEvent(int fd, unsigned int events, IOEventHandler const& handler) {
      if (static_cast<unsigned>(fd) >= ioEvents_.size()) {
	ioEvents_.resize(fd + 1);
      }

      events &= (EV_READABLE | EV_WRITABLE); // clean events to contaion only EV_READABLE or EV_WRITABLE
      unsigned int oldEvents = ioEvents_[fd].events_;
      if (oldEvents != events) {
	poll_.addEvent(fd, events, oldEvents);
	ioEvents_[fd].events_ = events;
      }

      if (events & EV_READABLE) ioEvents_[fd].readHandler_ = handler;
      if (events & EV_WRITABLE) ioEvents_[fd].writeHandler_ = handler;
    }

    template <typename P>
    void EventLoop<P>::delIOEvent(int fd, unsigned int events) {
      BOOST_ASSERT((unsigned)fd < ioEvents_.size());

      unsigned int oldEvents = ioEvents_[fd].events_;
      if (events == EV_ALL) {
	events = oldEvents;
      } else if ((events & oldEvents) == EV_NONE) {
	//no events to be deleted, just return
	//or throw an exception?
	return;
      }

      events &= (EV_READABLE | EV_WRITABLE);
      poll_.delEvent(fd, events, oldEvents);
      ioEvents_[fd].events_ &= ~events;
    }

    template <typename P>
    void EventLoop<P>::rock() {
      keepLoop_ = true;
      std::vector<FiredIOEvent> firedEvents;
      while (keepLoop_) {
	if (!timeEvents_.empty()) {
	  struct timeval timeout = timeEvents_.top().timeout();
	  poll_.poll(&timeout, firedEvents);
	} else {
	  poll_.poll(nullptr, firedEvents);
	}

	//processing IO events
	for (auto const& fired : firedEvents) {
	  IOEvent& ev = ioEvents_[fired.fd_];
	  unsigned int mask = fired.events_ & ev.events_;
	  if ((mask & EV_READABLE) && ev.readHandler_) {
	    ev.readHandler_(*this, fired.fd_, EV_READABLE);
	  }
	  if ((mask & EV_WRITABLE) && ev.writeHandler_) {
	    ev.writeHandler_(*this, fired.fd_, EV_WRITABLE);
	  }
	}

	//processing time events
	while (!timeEvents_.empty()) {
	  TimeEvent const &te = timeEvents_.top();
	  if (te.isExpired() && te.handler_) {
	    long long nextTime = te.handler_(*this, te.id_);
	    if (nextTime > 0) {
	      //We need to pop first before push,
	      //so we have to create a temporary object.
	      TimeEvent te2(te); 

	      te2.setExpireTime(nextTime);
	      timeEvents_.pop();
	      timeEvents_.push(te2);
	    } else {
	      timeEvents_.pop();
	    }
	  } else {
	    break;
	  }
	}
      }
    }

  }
}

#endif
